package gin

import (
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"sort"
)

const (
	AuthUserKey = "user"
)

type (
	BasicAuthPair struct {
		Code string
		User string
	}
	Accounts map[string]string
	Pairs    []BasicAuthPair
)

//****** 定义排序规则 ******

// Len 用于返回集合的长度
// sort包需要知晓集合的大小,从而确定排序的范围
func (a Pairs) Len() int {
	return len(a)
}

// Swap 用于交换索引i和j的元素
// 根据Less方法的比较结果,调用Swap调整元素的顺序
func (a Pairs) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

// Less 用于比较索引i和j的元素
// 如果索引为i的元素必须排在索引为j的元素之间,则返回true
// 若Less(i,j)和Less(j,i)都为false,则认为i,j元素相等,借助sort.Stable保留相等元素的原始输入顺序
func (a Pairs) Less(i, j int) bool {
	return a[i].Code < a[j].Code
}

// processCredentials 处理登录凭据
func processCredentials(accounts Accounts) (Pairs, error) {
	// 目前用的临时本地键值对作为登录凭证
	if len(accounts) == 0 {
		return nil, errors.New("empty list of authorized credentials")
	}
	pairs := make(Pairs, 0, len(accounts))
	// 使用range遍历map时,两个参数则是key和value
	// 这里的user和password便是map中的键和值
	for user, password := range accounts {
		if len(user) == 0 || len(password) == 0 {
			return nil, errors.New("user or password is empty")
		}
		base := user + ":" + password

		// 将用户名与密码组合进行base64编码
		// base64是用64个可打印字符来标识二进制数据的编码方式
		// 常用于在文本协议里传输二进制数据,像在HTTP协议中传输图片,文件等
		// base64编码可以规避不同系统平台间二进制数据的兼容性以及避免数据损坏(如二进制中特殊字符可能被系统或协议特殊处理)
		// 同时也方便数据库存储(部分数据库对二进制数据存储有一定限制)
		code := "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
		fmt.Println("code:", code)
		pairs = append(pairs, BasicAuthPair{code, user})
		fmt.Println("pairs:", pairs)
	}
	// 必须对凭据进行排序，以便以后使用bsearch
	// 以code为基准,code值小的在前面
	sort.Sort(pairs)
	fmt.Println("排序后:", pairs)
	return pairs, nil
}

func searchCredential(pairs Pairs, auth string) string {
	fmt.Println("客户端传入的凭证:", auth)
	if len(auth) == 0 {
		return ""
	}

	// 二分查找满足条件的第一个索引位置(code>auth)
	r := sort.Search(len(pairs), func(i int) bool {
		fmt.Printf("pairs[%d].Code:%s", i, pairs[i].Code)
		return pairs[i].Code >= auth
	})
	//fmt.Println("满足条件的索引:", r)

	// ConstantTimeCompare 用于比较切片是否相等(防止时间侧信道攻击)
	// 该函数会遍历两个字节切片的所有位置，无论是否已经能确定他们不相等，都不会提前结束比较
	// 相等则返回1
	if r < len(pairs) && subtle.ConstantTimeCompare([]byte(pairs[r].Code), []byte(auth)) == 1 {
		// 认证成功,将user存入当前上下文中,方便后续使用
		// c.Set("user"...)
		return pairs[r].User
	} else {
		return ""
	}
}

// BasicAuth 实现基本的HTTP授权,将map[string]string作为参数
// 其中键是用户名,值是密码
func BasicAuth(accounts Accounts) HandlerFunc {
	pairs, err := processCredentials(accounts)
	if err != nil {
		panic(err)
	}
	return func(c *Context) {
		user := searchCredential(pairs, c.Req.Header.Get("Authorization"))
		if len(user) == 0 {
			// 认证不匹配,返回401并中断当前请求
			// 大多浏览器在接受这个响应头后,后续会弹出一个认证表单
			// 表单会将用户名和密码用:连接成字符串base64编码后发送给服务器
			// 同时会加上Basic 前缀,放在Authorization请求头中
			c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
			c.Fail(401, errors.New("unauthorized"))
		} else {
			// 成功则将user存入上下文,方便后续使用
			c.Set(AuthUserKey, user)
		}
	}
}
